iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Software Development

Haskell 從入門到放棄系列 第 9

[Haskell 從入門到放棄] Day 09 - 終於來到 higher order function

  • 分享至 

  • xImage
  •  

終於來到一個跟 FP 的核心概念有關的特性了,首先我們需要知道 Haskell 是具有 First-class function 這個特性,意思是指 function 可以把它當一般的變數來看,我們可以把 function 存在某個資料結構中、當作 function 回傳值、把變數當作參數等等跟變數一樣的行為。

什麼是 Higher order function

簡單來說就是 function 當作 function 參數以及 function 回傳 function 這件事情。也就是說因為有 First-class function 這個特性我們才能這樣使用 function 。

在使用 Haskell 之前,這邊先用我比較熟悉的程式語言來說明這個特性

const add5 = (x) => x + 5;
const list = [1,2,3];

console.log(list.map(add5)) // [6,7,8]

我們這邊直接傳入 add5 當作 map 的參數,[list.map](http://list.map) 參數的 type 是 (a:number) => number ,所以我們可以直接傳入 add5 ,如果沒有這個特性大概會變成是這樣寫

const add5 = (x) => x + 5;
const list = [1,2,3];

for(let i = 0; i<list.length ; i++){
		list[i] = add5(list[i])
}
console.log(list) // [6,7,8]

當然這個範例的差異還有一個是 imuttable 而另一個是 muttable ,但我們先把重點放在 function 就好。

Curried function

簡單來說明這個概念就是,對於多參數 function 來說,我們可以不用一次傳入所有的參數,如果沒有傳完會先回傳 function 直到所有的參數都傳入為止。

首先有一個概念在 Haskell 中所有 function 都是 curried function,也可以這麼說「在 Haskell 中所有函數都只有一個參數,然後也許再回傳一個 function 或者回傳一個數值」

add x y = x + y

add 3 5 -- 8

(add 3) 5 -- 8

這邊可以得知我直接傳入 3 5 跟先傳入 3 再傳入5 是一樣的,也就是(add 3 ) 這個expression 是會回傳一個 function 。

但其實 add 3 5 也算是先傳入 3 再傳入 5 就是了

整個流程詳細解釋的話大概會像是這樣:

首先我們已經知道 add:: Num a => a -> a -> a
所以 (add 3 ) :: Num a => a -> a
(add 3) 5 就是顯而易見就是 :: Num a => a

那 curried function 的好處是什麼呢?我認為最大的用途我可以用一個 function 快速建構另外一個 function

add3 = add 3

add3 5 -- 8
add3 10 -- 13

運用這種特性可以讓我們更好的 reuse 或者組合 function。

那就算是 infix function 我們也是能夠不用一次傳入所有function

product5 = (*5) 
prdouct5 10 -- 50

isUpperChar = (`elem`) ['A'..'Z']
isUpperChar 'a' -- Flase
isUpperChar 'B' -- True

我們可以用 () 包住 function 這表示我要馬上呼叫他,首先一樣要是那個前提「在 Haskell 中所有函數都只有一個參數,然後也許再回傳一個 function 或者回傳一個數值」

  1. (*) :: Num a => a -> a -> a
  2. (*5) :: Num a ⇒ a → a
  3. (*5) 10 :: Num a ⇒ a

綜合以上知識我們可以做出這些操作

transformStr :: (String -> String) -> [String] -> [String]
transformStr f s = case s of
    [] -> []
    (x:xs) -> f x : transformStr f xs

首先我們先宣告一個拿來處理字串的 function ,這裡可以看到他的 type 是
transformStr :: (String -> String) -> [String] -> [String]
這裡會看到一個新的型別 (String -> String) 為什麼突然會需要 () ?因為這樣我們這樣才知道這是在描述一個 function 的 type

print (transformStr (++ "!") ["foo", "bar", "baz"]) 
-- ["foo!","bar!","baz!"]

使用起來會像是這樣,我們傳入一個 function 是要串上 "!" 以及再傳入一個 list ["foo", "bar", "baz"]

還記得嗎我們第一個參數是 (String -> String)(++ "!") 剛好也是 String -> String ,這邊就可以看出來 curried function 的偉大之處!!

然後我們還可以更進一步封裝起來

strAddExclamation :: [String] -> [String]
strAddExclamation  = transformStr  (++ "!")

print (strAddExclamation ["foo", "bar", "baz"])
-- ["foo!","bar!","baz!"]

Map

其實我們剛剛的 transformStr 就有點像是 map 在做的事情,map 會傳入一個 function 及 List 然後讓 List 中的每個元素都執行那個 function 並回傳一個新的 List

:t map  -- map :: (a -> b) -> [a] -> [b]

map (+1) [1,2,3] -- [2,3,4]
map (elem 'f') ["foo","bar","baz"] -- [True,False,False]
map (*2) [x*2 | x <-[1..5]] -- [4,8,12,16,20]

map (++"!") ["foo","bar","baz"] ["foo!","bar!","baz!"]

由此可知 map 在 Haskell 中十分強大的 function ,因為它可以用很優雅的方式讓我們將一個 a 型別的 List 轉換成 b 型別的 List


今天我們聊到了 FP 中非常重要且經常用到的概念,而且會很常不知不覺地就是在使用了像是我使用 js/ts 開發時蠻常會使用到 array.map 或者 array.filter 幫我處理 array,或者是利用 higher order function 來幫我們減少程式碼的撰寫,特別像是在寫前端時很常會需要傳遞 function 到 component props

<Component
	onStateChange={(state)=>{
		handleStateChange(state)
   }}
/>

<Component
	onStateChange={handleStateChange}
/>

今天的程式碼:https://github.com/toddLiao469469/30days-for-haskell


上一篇
[Haskell 從入門到放棄] Day 08 - Pattern Matching (2)
下一篇
[Haskell 從入門到放棄] Day 10 - 再談 higher order function
系列文
Haskell 從入門到放棄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言